/*  ----------------------------------------------------------------------------
 *  Arello Mobile
 *  Fast 3d Engine
 *  ----------------------------------------------------------------------------
 * Except where otherwise noted, this work is licensed under
 * http://creativecommons.org/licenses/by/3.0
 */

#include "Channel.h"
#include "Hardware.h"
#include <logs/Log.h>
#include <math/utl.h>
#include <noise/Manager.h>
#include <noise/hard/Hardware.h>

namespace liba {
namespace noise {
namespace hard {
namespace openal {

static double calculate_attenuation(double distance, double min_distance, double max_distance)
{
	distance = math::clamp( distance, min_distance, max_distance );
	return pow( 2.0, -distance/min_distance);
}

static const int frequency_2d = 22050;
static const int frequency_3d = 22050;
static const int frequency_surround = 22050;
static const double minimum_update_part = 0.25;

ChannelStaticRep::ChannelStaticRep(const ChannelStaticRepName & name):
	size( 0 ),
	owned_by_distance(0),
	mem(0),
	source(0),
	buffer(0)
{
	source.reset(Manager::create_datasource( name.name ));
	if( !source.get() )
		THROW(GluckBadResource( __FUNCTION__, "Could not create DataSource for static channel", name.name ));

	memset( &wfx, 0, sizeof(WAV) );

	alGenBuffers( 1, &buffer );
	if( !buffer )
	{
		logs::log() << logs::error << "CreateSoundBuffer() failed. sound: " << name.name << "\n";
		return; // very bad...
	}

	if( name.channel_type == hard::Hardware::channel_type_2d )
	{
		wfx.nChannels = 2;
		wfx.nSamplesPerSec = source->get_frequency();
		wfx.wBitsPerSample = 16;
		wfx.nBlockAlign = wfx.wBitsPerSample / 8 * wfx.nChannels;
		wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign;
	}
	if( name.channel_type == hard::Hardware::channel_type_3d )
	{
		wfx.nChannels = 1;
		wfx.nSamplesPerSec = source->get_frequency();
		wfx.wBitsPerSample = 16;
		wfx.nBlockAlign = wfx.wBitsPerSample / 8 * wfx.nChannels;
		wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign;
	}
	if( name.channel_type == hard::Hardware::channel_type_surround )
	{
		THROW(Gluck( __FUNCTION__, "No surround channels here" ));
	}

	size = wfx.nBlockAlign * source->get_length();
	mem = new char[ size ];
	char * dst_ptr = mem;
	int required_samples = size / wfx.nBlockAlign;
	while( required_samples )
	{
		int decoded_samples = source->decode( source->get_length() );
		if( decoded_samples == 0 )
		{
			logs::log() << logs::error << "SoundSource failed to decode promissed number of samples: " << name.name << "\n";
			break; // very bad
		}
		required_samples -= decoded_samples;

		if( name.channel_type == hard::Hardware::channel_type_2d )
		{
			source->get_data( DataSource::STEREO_LEFT, dst_ptr, wfx.nBlockAlign, 16 );
			source->get_data( DataSource::STEREO_RIGHT, dst_ptr + 2, wfx.nBlockAlign, 16 );
		}
		if( name.channel_type == hard::Hardware::channel_type_3d )
		{
			source->get_data( DataSource::MONO, dst_ptr, wfx.nBlockAlign, 16 );
		}
		if( name.channel_type == hard::Hardware::channel_type_surround )
		{
			THROW(Gluck( __FUNCTION__, "No surround channels here" ));
		}
		dst_ptr += decoded_samples * wfx.nBlockAlign;
	}

	if( name.channel_type == hard::Hardware::channel_type_2d )
	{
		alBufferData( buffer, AL_FORMAT_STEREO16, mem, size, wfx.nSamplesPerSec );
	}
	if( name.channel_type == hard::Hardware::channel_type_3d )
	{
		alBufferData( buffer, AL_FORMAT_MONO16, mem, size, wfx.nSamplesPerSec );
	}
	
	delete [] mem; mem = 0;
	source.reset();

	//create source
	alGenSources( 1, &alsource );
	ALint error = alGetError();
	
	if( name.channel_type == hard::Hardware::channel_type_2d )
		alSourcei( alsource, AL_SOURCE_RELATIVE, AL_TRUE );
	
	// Attach buffer 0 to source
	alSourcei( alsource, AL_BUFFER, buffer );
}

ChannelStaticRep::~ChannelStaticRep()
{
	delete [] mem; mem = 0;

	if( buffer )
	{
		alDeleteBuffers( 1, &buffer );
		buffer = 0;
	}
	
	if(alsource)
	{
		alDeleteSources( 1, &alsource );
		alsource = 0;		
	}
}

Channel::Channel()
	: exact_volume( 1 )
	, exact_pan( 0 )
	, min_distance( 1 )
	, max_distance( 1E6 )
	, loop( true )
//	buffer( 0 ),
//	source( 0 )
{
	this->RegisterProperty(volume_name, &this->exact_volume);
	this->RegisterProperty(loop_name, &this->loop);
	this->RegisterProperty(pan_name, &this->exact_pan);
	this->RegisterProperty(minimum_distance_name, &this->min_distance);
	this->RegisterProperty(maximum_distance_name, &this->max_distance);

	this->RegisterProperty(position_name, &this->exact_position);
	this->RegisterProperty(speed_name, &this->exact_speed);
}

Channel::~Channel()
{}

ChannelStatic::ChannelStatic(Hardware * ha, const Atom & channel_type, const filesystem::String & name):
	finished( true ),
	approx_vol( 0 ),
	position( 0 ),
	channel_type( channel_type ),
	in_existance_flag(false)
{
	load_rep(ChannelStaticRepName(ha, channel_type, name));
	ha->add_static_channel(this);
}

ChannelStatic::~ChannelStatic()
{
	if( !get_rep() )
		return;

	get_rep()->set_owned_distance(0);

	in_existance_flag = false;
	
	stop();

	get_rep_name().hardware->remove_static_channel(this);
}


void ChannelStatic::life_cycle(double delta_time, const math::Vector3<float> & listener_position)
{
	if( !get_rep() )
		return;
	approx_vol = calculate_attenuation( exact_position.Get().length(listener_position), min_distance, max_distance );
	if( /*!buffer &&*/ !finished )
	{
		position += int(get_rep()->get_frequency() * delta_time) * get_rep()->get_block_align();
		if( position > (unsigned)get_rep()->get_size() )
			if( loop )
				position %= get_rep()->get_size();
			else
				finished = true;
	}
	if( in_existance_flag )
	{
		if( get_rep_name().channel_type == hard::Hardware::channel_type_3d )
		{
			ALfloat source0Pos[] = { exact_position.Get()[0], exact_position.Get()[1], exact_position.Get()[2] };
			alSourcefv( get_rep()->get_source(), AL_POSITION, source0Pos );
			
			ALfloat source0Vel[] = { exact_speed.Get()[0], exact_speed.Get()[1], exact_speed.Get()[2] };
			alSourcefv( get_rep()->get_source(), AL_VELOCITY, source0Vel );
			
			alSourcef( get_rep()->get_source(), AL_REFERENCE_DISTANCE, (ALfloat)min_distance );
			alSourcef( get_rep()->get_source(), AL_MAX_DISTANCE, (ALfloat)max_distance );
			alSourcef( get_rep()->get_source(), AL_ROLLOFF_FACTOR, 2 );
		}
		
		alSourcef( get_rep()->get_source(), AL_GAIN, (ALfloat)exact_volume );

		ALint state;
		alGetSourcei( get_rep()->get_source(), AL_SOURCE_STATE, &state);
		if( state != AL_PLAYING )
		{
			begone();
			finished = true;
		}
	}
}

void ChannelStatic::come_to_existance()
{
	if( !get_rep() )
		return;

	if( get_rep_name().channel_type == hard::Hardware::channel_type_3d )
	{
		if(get_rep()->get_owned_distance() >= approx_vol)
			return;
	}
	
	get_rep()->set_owned_distance(approx_vol);

	int pos = position/get_rep()->get_block_align();
	pos *= get_rep()->get_block_align();
	pos = 0;

	alSourcef( get_rep()->get_source(), AL_GAIN, (ALfloat)exact_volume );

	if( get_rep_name().channel_type == hard::Hardware::channel_type_3d )
	{
		ALfloat source0Pos[] = { exact_position.Get()[0], exact_position.Get()[1], exact_position.Get()[2] };
		alSourcefv( get_rep()->get_source(), AL_POSITION, source0Pos );

		ALfloat source0Vel[] = { exact_speed.Get()[0], exact_speed.Get()[1], exact_speed.Get()[2] };
		alSourcefv( get_rep()->get_source(), AL_VELOCITY, source0Vel );

		alSourcef( get_rep()->get_source(), AL_REFERENCE_DISTANCE, (ALfloat)min_distance );
		alSourcef( get_rep()->get_source(), AL_MAX_DISTANCE, (ALfloat)max_distance );
		alSourcef( get_rep()->get_source(), AL_ROLLOFF_FACTOR, 2 );
	}

	alSourcei( get_rep()->get_source(), AL_LOOPING, loop ? AL_TRUE : AL_FALSE );
	alSourcePlay( get_rep()->get_source() );
	in_existance_flag = true;
}

void ChannelStatic::begone()
{
	if(!in_existance_flag)
		return;

	in_existance_flag = false;
	
	if( !get_rep() )
		return;

	get_rep()->set_owned_distance(0);

	alSourceStop( get_rep()->get_source() );
}

void ChannelStatic::play(DataSource * source)
{
	if( !get_rep() )
		return;
	stop();
	position = 0;
	finished = false;

	if( get_rep_name().channel_type == hard::Hardware::channel_type_3d )
	{
		get_rep_name().hardware->play_static_3d_channel(this);
	}
	else
	{
		come_to_existance();
	}
}

void ChannelStatic::stop()
{
	if( !get_rep() )
		return;
	if( get_rep_name().channel_type == hard::Hardware::channel_type_3d )
	{
		get_rep_name().hardware->stop_static_3d_channel(this);
	}
	if( in_existance() )
		begone();

	finished = true;
	position = 0;
}

bool ChannelStatic::is_finished()
{
	if( !get_rep() )
		return true;
	if( get_rep_name().channel_type != hard::Hardware::channel_type_3d )
		life_cycle(0, math::Vector3<float>() );
	return finished;
}

void ChannelStatic::on_hardware_delete()
{
	stop();
	unload_rep();
}

ChannelStreaming::ChannelStreaming(Hardware * hardware, const Atom & channel_type, double length_sec):
	hardware( hardware ),
	channel_type( channel_type )
{
	memset( &wfx, 0, sizeof(WAV) );

	if( channel_type == hard::Hardware::channel_type_2d )
	{
		wfx.nChannels = 2;
		wfx.nSamplesPerSec = frequency_2d;
		wfx.wBitsPerSample = 16;
		wfx.nBlockAlign = wfx.wBitsPerSample / 8 * wfx.nChannels;
		wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign;
	}
	if( channel_type == hard::Hardware::channel_type_3d )
	{
		wfx.nChannels = 1;
		wfx.nSamplesPerSec = frequency_3d;
		wfx.wBitsPerSample = 16;
		wfx.nBlockAlign = wfx.wBitsPerSample / 8 * wfx.nChannels;
		wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign;
	}
	if( channel_type == hard::Hardware::channel_type_surround )
	{
		THROW(Gluck( __FUNCTION__, "No surround here" ));
	}

	alGenBuffers( NUMBUFFERS, buffers );
	ALint error = alGetError();
	if( error != AL_NO_ERROR )
	{
		THROW(Gluck( __FUNCTION__, "Streaming channel failed" ));
	}
	size = wfx.nBlockAlign * int(wfx.nSamplesPerSec * length_sec);
	minimum_update_size = unsigned(size * minimum_update_part);

	alGenSources( 1, &source );
	error = alGetError();
	if( error )
	{
		THROW(Gluck( __FUNCTION__, "Streaming Channel: openal: Out of sources" ));
	}

	if( channel_type == hard::Hardware::channel_type_2d )
	{
		alSourcei( source, AL_SOURCE_RELATIVE, AL_TRUE );
	}

	playing = false;
	hardware->add_streaming_channel( this );
}

ChannelStreaming::~ChannelStreaming()
{
	if( hardware )
		hardware->remove_streaming_channel( this );

	if( source )
		alDeleteSources( 1, &source );
	source = 0;

	destroy_sources();
	if( buffers[0] )
		alDeleteBuffers( NUMBUFFERS, buffers );
	buffers[0] = 0;
}

void ChannelStreaming::destroy_sources()
{
	while( sources.size() )
	{
		delete sources.back();
		sources.pop_back();
	}
}

void ChannelStreaming::play(DataSource * source)
{
	if( !source )
		return; // ok - added empty datasource
	sources.push_back( source );
	if( sources.size() == 1 ) // first buffer, sound can still be playing...
	{
		if( buffers[0] )
		{
			for( int i = 0; i < NUMBUFFERS; ++i )
				update( buffers[i] );
			// Queue the buffers on the source
			alSourceQueueBuffers( this->source, NUMBUFFERS, buffers );
			ALint error = alGetError();
			alSourcePlay( this->source );
			playing = true;
			life_cycle(0);
			buffers_in_queue = NUMBUFFERS;
		}
	}
}

void ChannelStreaming::stop()
{
	playing = false;
	alSourceStop( source );
	ALuint BufferID[NUMBUFFERS];
	alSourceUnqueueBuffers( source, NUMBUFFERS, BufferID );
	destroy_sources();
}

bool ChannelStreaming::is_finished()
{
	return !playing;
}

bool ChannelStreaming::update( ALuint buffer_id )
{
	int required_samples = BSIZE / wfx.nBlockAlign;
	int original_req_samples = required_samples;
	char * dst_ptr = reinterpret_cast<char *>( temp_data );
	int s = 0;
	while( required_samples && (s = sources.size()) )
	{
		int decoded_samples = sources.front()->decode( required_samples );
		if( decoded_samples == 0 )
		{
			if( sources.size() > 1 || !loop)
			{
				delete sources.front();
				sources.pop_front();
				continue;
			}
			continue;
		}
		required_samples -= decoded_samples;

		if( channel_type == hard::Hardware::channel_type_2d )
		{
			sources.front()->get_data( DataSource::STEREO_LEFT, dst_ptr, wfx.nBlockAlign, 16 );
			sources.front()->get_data( DataSource::STEREO_RIGHT, dst_ptr + 2, wfx.nBlockAlign, 16 );
		}
		if( channel_type == hard::Hardware::channel_type_3d )
		{
			sources.front()->get_data( DataSource::MONO, dst_ptr, wfx.nBlockAlign, 16 );
		}
		dst_ptr += decoded_samples * wfx.nBlockAlign;
		silence_size = 0;
	}
	if( required_samples )
	{
		silence_size += required_samples * wfx.nBlockAlign;
		memset( dst_ptr, 0, required_samples * wfx.nBlockAlign );
	}

	if( channel_type == hard::Hardware::channel_type_2d )
	{
		alBufferData( buffer_id, AL_FORMAT_STEREO16, temp_data, BSIZE, wfx.nSamplesPerSec );
	}
	if( channel_type == hard::Hardware::channel_type_3d )
	{
		alBufferData( buffer_id, AL_FORMAT_MONO16, temp_data, BSIZE, wfx.nSamplesPerSec );
	}
	ALint error = alGetError();

	return required_samples != original_req_samples;
}

void ChannelStreaming::life_cycle(double delta_time)
{
	if( !buffers[0] || !playing || !source )
		return;

	// Get status
	ALint processed;
	alGetSourcei( source, AL_BUFFERS_PROCESSED, &processed );

	ALint state;
	alGetSourcei( source, AL_SOURCE_STATE, &state);
	if( state != AL_PLAYING )
	{
		//buffer stopped...need to restart it

		// Unqueue all buffers
		alGetSourcei( source, AL_BUFFERS_PROCESSED, &processed );
	}

	// If some buffers have been played, unqueue them and load new audio into them, then add them to the queue
	while( processed )
	{
		ALuint BufferID;
		alSourceUnqueueBuffers( source, 1, &BufferID );
		ALint error = alGetError();

		if( update( BufferID ) )
		{
			// Queue buffer
			alSourceQueueBuffers( source, 1, &BufferID );
			error = alGetError();
		}
		else
			--buffers_in_queue;

		if( !buffers_in_queue )
		{
			playing = false;
			alSourceStop( source );
		}

		--processed;
	}

	if( state != AL_PLAYING )
	{
		// Restart playback
		alSourcePlay( source );
	}
}

void ChannelStreaming::on_hardware_delete()
{
	alSourceStop( source );
	if( buffers[0] )
		alDeleteBuffers( NUMBUFFERS, buffers );
	buffers[0] = 0;
	hardware = 0;
	playing = false;
}

} // namespace openal
} // namespace hard
} // namespace noise
} // namesapce liba

